/**
 * Spinning cube in Molehill.
 * http://ltslashgt.com/2011/02/28/molehill-spinning-cube/
 */
package {
    import com.adobe.utils.AGALMiniAssembler;
    
    import flash.display.Sprite;
    import flash.display.Stage3D;
    import flash.display.StageAlign;
    import flash.display.StageScaleMode;
    import flash.display3D.Context3D;
    import flash.display3D.Context3DCompareMode;
    import flash.display3D.Context3DProgramType;
    import flash.display3D.Context3DRenderMode;
    import flash.display3D.Context3DTriangleFace;
    import flash.display3D.Context3DVertexBufferFormat;
    import flash.display3D.IndexBuffer3D;
    import flash.display3D.Program3D;
    import flash.display3D.VertexBuffer3D;
    import flash.events.Event;
    import flash.geom.Matrix3D;
    import flash.geom.Rectangle;
    import flash.geom.Vector3D;
    import flash.utils.getTimer;
    
    [SWF(frameRate="60", width="1024", height="576")]
    public class SpinningCubeApp extends Sprite {
        protected var _width:Number;
        protected var _height:Number;
        
        protected var _stage:Stage3D;
        protected var _context:Context3D;
        protected var _indexBuffer:IndexBuffer3D;
        protected var _vertexBuffer:VertexBuffer3D;
        protected var _colorVertexBuffer:VertexBuffer3D;
        protected var _program:Program3D;
        
        protected var _modelView:Matrix3D;
        protected var _modelViewProjection:Matrix3D;
        protected var _projection:Matrix3D;
        
        protected var _time:Number = 0;
        protected var _deltaTime:Number = 0;
        protected var _tweenTime:Number = 0;
        protected var _tweenPitch:Number = 0;
        protected var _tweenYaw:Number = 0;
        protected var _pitch:Number = 0;
        protected var _yaw:Number = 0;
        
        // Vertexes for the cube. Format is (x, y, z, r, g, b), counter-clockwise
        // winding, with normal OpenGL axes (-x/+x = left/right, -y/+y = bottom/top,
        // -z/+z = far/near). This is also a bit overkill on vertexes, but the
        // extras are needed to make each side a solid color.
        protected var _cubeVertexes:Vector.<Number> = Vector.<Number>([
            // near face
            -1.0, -1.0, 1.0,
            -1.0, 1.0, 1.0,
            1.0, 1.0, 1.0,
            1.0, -1.0, 1.0,
            
            // left face
            -1.0, -1.0, -1.0,
            -1.0, 1.0, -1.0,
            -1.0, 1.0, 1.0,
            -1.0, -1.0, 1.0,
            
            // far face
            1.0, -1.0, -1.0,
            1.0, 1.0, -1.0,
            -1.0, 1.0, -1.0,
            -1.0, -1.0, -1.0,
            
            // right face
            1.0, -1.0, 1.0,
            1.0, 1.0, 1.0,
            1.0, 1.0, -1.0,
            1.0, -1.0, -1.0,
            
            // top face
            -1.0, 1.0, 1.0,
            -1.0, 1.0, -1.0,
            1.0, 1.0, -1.0,
            1.0, 1.0, 1.0,
            
            // bottom face
            -1.0, -1.0, -1.0,
            -1.0, -1.0, 1.0,
            1.0, -1.0, 1.0,
            1.0, -1.0, -1.0
        ]);
        
        protected var _colorVertexes:Vector.<Number> = Vector.<Number>([
            1.0,1.0,1.0,
            1.0,1.0,1.0,
            1.0,1.0,1.0,
            1.0,1.0,1.0,
            
            0.0,1.0,1.0,
            0.0,1.0,1.0,
            0.0,1.0,1.0,
            0.0,1.0,1.0,
            
            1.0,1.0,1.0,
            1.0,1.0,1.0,
            1.0,1.0,1.0,
            1.0,1.0,1.0,
            
            0.0,1.0,1.0,
            0.0,1.0,1.0,
            0.0,1.0,1.0,
            0.0,1.0,1.0,
            
            1.0,1.0,1.0,
            1.0,1.0,1.0,
            1.0,1.0,1.0,
            1.0,1.0,1.0,
            
            0.0,1.0,1.0,
            0.0,1.0,1.0,
            0.0,1.0,1.0,
            0.0,1.0,1.0
        ]);
        // Indexes into the vertex buffer above for each of the cube's triangles.
        protected var _cubeIndexes:Vector.<uint> = Vector.<uint>([
            0, 1, 2,
            0, 2, 3,
            4, 5, 6,
            4, 6, 7,
            8, 9, 10,
            8, 10, 11,
            12, 13, 14,
            12, 14, 15,
            16, 17, 18,
            16, 18, 19,
            20, 21, 22,
            20, 22, 23
        ]);
        
        protected var _vertexShader:String = [
            "m44 op, va0, vc0", // multiply vertex by modelViewProjection
            "move v0, va1"
        ].join("\n");
        
        protected var _fragmentShader:String = [
            "mov oc, v0" // output the fragment color
        ].join("\n");
        
        public function SpinningCubeApp() {
            addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
        }
        
        protected function onAddedToStage(event:Event):void {
            stage.align = StageAlign.TOP_LEFT;
            stage.scaleMode = StageScaleMode.NO_SCALE;
            
            _width = stage.stageWidth;
            _height = stage.stageHeight;
            
            _stage = stage.stage3Ds[0];
            _stage.addEventListener(Event.CONTEXT3D_CREATE, onContext);
            _stage.requestContext3D(Context3DRenderMode.AUTO);
            _stage.x = 0;
            _stage.y = 0;
            
            //_stage.viewPort = new Rectangle(0, 0, _width, _height);
        }
        
        // Called once the context we requested has been created.
        protected function onContext(event:Event):void {
            _context = _stage.context3D;
            _context.configureBackBuffer(_width, _height, 2, true);
            _context.enableErrorChecking = true;
            // Discard triangles pointing away from the camera, and ones
            // behind things that we've already drawn.
            _context.setCulling(Context3DTriangleFace.BACK);
            _context.setDepthTest(true, Context3DCompareMode.LESS_EQUAL);
            
            // Setup our initial matrices.
            _modelView = new Matrix3D;
            _modelViewProjection = new Matrix3D;
            _projection = perspectiveProjection(60, _width / _height, 0.1, 2048);
            
            // Create a program from the two shaders.
            var vsAssembler:AGALMiniAssembler = new AGALMiniAssembler;
            vsAssembler.assemble(Context3DProgramType.VERTEX, _vertexShader);
            var fsAssembler:AGALMiniAssembler = new AGALMiniAssembler;
            fsAssembler.assemble(Context3DProgramType.FRAGMENT, _fragmentShader);
            _program = _context.createProgram();
            _program.upload(vsAssembler.agalcode, fsAssembler.agalcode);
            _context.setProgram(_program);
            
            // Upload all the vertex data and create two streams. Position data
            // will be available to the vertex program as `va0`, color data as `va1`.
            _vertexBuffer = _context.createVertexBuffer(_cubeVertexes.length / 3, 3);
            _vertexBuffer.uploadFromVector(_cubeVertexes, 0, _cubeVertexes.length / 3);
            
            _colorVertexBuffer = _context.createVertexBuffer(_colorVertexes.length / 3, 3);
            _colorVertexBuffer.uploadFromVector(_colorVertexes, 0, _colorVertexes.length / 3);
            
            _context.setVertexBufferAt(0, _vertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_3);
            _context.setVertexBufferAt(1, _colorVertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_3);
            
            // Upload the index data. This is used by drawTriangles (below).
            _indexBuffer = _context.createIndexBuffer(_cubeIndexes.length);
            _indexBuffer.uploadFromVector(_cubeIndexes, 0, _cubeIndexes.length);
            
            _time = getTimer() / 1000.0;
            _tweenTime = _time + 1;
            addEventListener(Event.ENTER_FRAME, onEnterFrame);
        }
        
        protected function onEnterFrame(event:Event):void {
            _context.clear(0, 0, 0);
            
            var newTime:Number = getTimer() / 1000.0;
            _deltaTime = Math.min(newTime - _time, 0.1);
            _time = newTime;
            
            updateRotation();
            
            // Update the modelView matrix given the current rotation
            _modelView.identity();
            _modelView.appendRotation(_tweenPitch, Vector3D.X_AXIS);
            _modelView.appendRotation(_tweenYaw, Vector3D.Y_AXIS);
            _modelView.appendTranslation(0, 0, -4);
            
            // Concatenate the modelView and projection matrices and set it as
            // a vertex program constant. It will be available as `vc0`.
            _modelViewProjection.identity();
            _modelViewProjection.append(_modelView);
            _modelViewProjection.append(_projection);
            _context.setProgramConstantsFromMatrix(
                Context3DProgramType.VERTEX, 0, _modelViewProjection, true);
            
            // Tell the GPU to draw all the triangles in indexBuffer. You can also
            // specify an offset and limit within the index buffer, if desired.
            _context.drawTriangles(_indexBuffer);
            
            _context.present();
        }
        
        protected function perspectiveProjection(fov:Number=90,
                                                 aspect:Number=1, near:Number=1, far:Number=2048):Matrix3D {
            var y2:Number = near * Math.tan(fov * Math.PI / 360);
            var y1:Number = -y2;
            var x1:Number = y1 * aspect;
            var x2:Number = y2 * aspect;
            
            var a:Number = 2 * near / (x2 - x1);
            var b:Number = 2 * near / (y2 - y1);
            var c:Number = (x2 + x1) / (x2 - x1);
            var d:Number = (y2 + y1) / (y2 - y1);
            var q:Number = -(far + near) / (far - near);
            var qn:Number = -2 * (far * near) / (far - near);
            
            return new Matrix3D(Vector.<Number>([
                a, 0, 0, 0,
                0, b, 0, 0,
                c, d, q, -1,
                0, 0, qn, 0
            ]));
        }
        
        protected function updateRotation():void {
            while (_tweenTime < _time) {
                _tweenTime += 1;
                _pitch = (_pitch + 60) % 360;
                _yaw = (_yaw + 40) % 360;
            }
            
            var factor:Number = _tweenTime - _time;
            if (factor < 0.0) factor = 0.0;
            else if (factor > 1.0) factor = 1.0;
            factor = 1.0 - Math.pow(factor, 4);
            
            _tweenPitch = _pitch + (60 * factor);
            _tweenYaw = _yaw + (40 * factor);
        }
    }
}